﻿using System;
using System.Linq;
using System.Collections.Generic;

namespace XDCCManager.Model
{
	public class ActionViewObservableCollection : ICollection<Packs.IPack>, System.ComponentModel.INotifyPropertyChanged, System.Collections.Specialized.INotifyCollectionChanged, Packs.IPackList
	{
		private Packs.INavigable sourceEntryPoint;
		private Packs.INavigable destinationEntryPoint;
		private Packs.INavigable newEntryPoint;
		private IDictionary<Packs.Pack, Packs.IPack> sourceMapping;
		private SortedObservableCollection<Packs.Pack> source;

		internal ActionViewObservableCollection(SortedObservableCollection<Packs.Pack> source)
		{
			this.source = source;
			sourceMapping = new Dictionary<Packs.Pack, Packs.IPack>();
			Packs.INavigable previousSourcePlaceHolder = null;
			Packs.INavigable previousDestinationPlaceHolder = null;
			for (int i = 0; i < source.Count; i++)
			{
				Packs.Pack element = source[i];
				InsertPackAt(ref previousSourcePlaceHolder, ref previousDestinationPlaceHolder, element);
			}
			source.CollectionChanged += source_CollectionChanged;
		}

		private void InsertPackAt(ref Packs.INavigable previousSource, ref Packs.INavigable previousDestination, Packs.Pack element)
		{
			Packs.INavigable source = new Packs.PlaceHolderPack() { Source = element };
			Packs.INavigable destination = new Packs.MovedPack() { Source = source as Packs.IPack };
			source.Destination = destination as Packs.IPack;
			source.Before = previousSource;
			if (previousSource != null)
			{
				if (previousSource.After != null)
				{
					source.After = previousSource.After;
					source.After.Before = source;
				}
				previousSource.After = source;
			}
			destination.Before = previousDestination;
			if (previousDestination != null)
			{
				if (previousDestination.After != null)
				{
					destination.After = previousDestination.After;
					destination.After.Before = destination;
				}
				previousDestination.After = destination;
			}
			if (this.sourceEntryPoint == null)
				this.sourceEntryPoint = source;
			if (this.destinationEntryPoint == null)
				this.destinationEntryPoint = destination;
			this.sourceMapping.Add(element, source as Packs.IPack);
			previousSource = source;
			previousDestination = destination;
		}

		void source_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
		{
			switch (e.Action)
			{
				case System.Collections.Specialized.NotifyCollectionChangedAction.Add:
					Add(e.NewItems, e.NewStartingIndex);
					break;
				case System.Collections.Specialized.NotifyCollectionChangedAction.Remove:
					Remove(e.OldItems, e.OldStartingIndex);
					break;
			}
		}

		private void Remove(System.Collections.IList items, int start)
		{
			List<Packs.IPack> removedItems = new List<Packs.IPack>();
			foreach (var pack in items.OfType<Packs.Pack>())
			{
				if (!sourceMapping.ContainsKey(pack))
					continue;
				Packs.INavigable removed = sourceMapping[pack] as Packs.INavigable;
				while ((removed != null) && (removed.Destination != null))
				{
					if (removed is Packs.PlaceHolderPack)
					{
						if (removed.Before == null)
							sourceEntryPoint = removed.After;
					}
					if (removed.Before != null)
						removed.Before.After = removed.After;
					if (removed.After != null)
						removed.After.Before = removed.Before;
					removed.Before = null;
					removed.After = null;
					Packs.IPack destPack = removed.Destination;
					removed.Destination = null;
					removed.Source = null;
					removed = destPack as Packs.INavigable;
				}
				removedItems.Add(removed as Packs.IPack);
			}
			if (removedItems.Count > 0)
			{
				foreach (var item in removedItems)
				{
					Packs.INavigable removed = item as Packs.INavigable;
					if (removed.Before != null)
						removed.Before.After = removed.After;
					else
						destinationEntryPoint = removed.After;
					if (removed.After != null)
						removed.After.Before = removed.Before;
					removed.Before = null;
					removed.After = null;
					Packs.IPack destPack = removed.Destination;
					removed.Destination = null;
					removed.Source = null;
					Count--;
					OnCollectionChanged(new System.Collections.Specialized.NotifyCollectionChangedEventArgs(System.Collections.Specialized.NotifyCollectionChangedAction.Remove, item, getIndex(removed)));
				}
			}
		}

		private void Add(System.Collections.IList items, int start)
		{
			Packs.INavigable previousSource = null;
			Packs.INavigable previousDestination = null;
			int realStart = 0;
			Packs.INavigable source = this.sourceEntryPoint;
			Packs.INavigable destination = this.destinationEntryPoint;
			if ((start > 0) && (this.sourceEntryPoint != null))
			{
				while (source != null)
				{
					previousSource = source;
					previousDestination = destination;
					realStart++;
					if (realStart >= start)
						break;
					source = source.After;
					destination = destination.After;
				}
			}
			source = this.sourceEntryPoint;
			destination = this.destinationEntryPoint;
			if (realStart == 0)
			{
				this.sourceEntryPoint = null;
				this.destinationEntryPoint = null;
			}
			IList<Packs.INavigable> addedItems = new List<Packs.INavigable>(items.Count);
			for (int i = 0; i < items.Count; i++)
			{
				Packs.Pack pack = items[i] as Packs.Pack;
				if (pack == null)
					continue;
				InsertPackAt(ref previousSource, ref previousDestination, pack);
				addedItems.Add(previousDestination);
			}
			if ((realStart == 0) && (source != null))
			{
				if (previousSource != null)
				{
					previousSource.After = source;
					source.Before = previousSource;
					previousDestination.After = destination;
					destination.Before = previousDestination;
				}
				else
				{
					this.sourceEntryPoint = source;
					this.destinationEntryPoint = destination;
				}
			}
			if (addedItems.Count > 0)
				OnCollectionChanged(new System.Collections.Specialized.NotifyCollectionChangedEventArgs(System.Collections.Specialized.NotifyCollectionChangedAction.Add, (System.Collections.IList)addedItems, realStart));
			Count += addedItems.Count;
		}

		private int count;

		public int Count
		{
			get { return count; }
			private set { if (count != value) { count = value; OnPropertyChanged("Count"); } }
		}

		#region ICollection<IPack> Members

		public void Add(Packs.IPack item)
		{
			throw new InvalidOperationException();
		}

		public void Clear()
		{
			throw new InvalidOperationException();
		}

		public bool Contains(Packs.IPack item)
		{
			throw new InvalidOperationException();
		}

		public void CopyTo(Packs.IPack[] array, int arrayIndex)
		{
			Packs.INavigable element = destinationEntryPoint;
			while ((element != null) && (arrayIndex < array.Length))
			{
				array[arrayIndex++] = element as Packs.IPack;
				element = element.After;
			}
		}

		public bool IsReadOnly
		{
			get { return true; }
		}

		public bool Remove(Packs.IPack item)
		{
			throw new InvalidOperationException();
		}

		#endregion

		#region IEnumerable<IPack> Members

		public IEnumerator<Packs.IPack> GetEnumerator()
		{
			if (destinationEntryPoint == null)
				yield break;
			Packs.INavigable start = destinationEntryPoint;
			do
			{
				yield return start as Packs.IPack;
				start = start.After;
			}
			while (start != null);
		}

		#endregion

		#region IEnumerable Members

		System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
		{
			if (destinationEntryPoint == null)
				yield break;
			Packs.INavigable start = destinationEntryPoint;
			do
			{
				yield return start;
				start = start.After;
			}
			while (start != null);
		}

		#endregion

		#region INotifyPropertyChanged Members

		public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;

		#endregion

		private void OnCollectionChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs args)
		{
			if (CollectionChanged != null)
				CollectionChanged(this, args);
		}

		#region INotifyCollectionChanged Members

		public event System.Collections.Specialized.NotifyCollectionChangedEventHandler CollectionChanged;

		#endregion

		private void OnPropertyChanged(string name)
		{
			if (PropertyChanged != null)
				PropertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(name));
		}

		public Actions.IAction AddFile(Files.File source)
		{
			Packs.NewPack newPack = new Packs.NewPack() { Description = source.Name, Size = source.Size };
			Packs.PlaceHolderPack phPack = new Packs.PlaceHolderPack() { Source = newPack };
			Packs.MovedPack mPack = new Packs.MovedPack() { Source = phPack };
			AppendPack(newPack, ref newEntryPoint);
			AppendPack(phPack, ref sourceEntryPoint);
			AppendPack(mPack, ref destinationEntryPoint);
			OnCollectionChanged(new System.Collections.Specialized.NotifyCollectionChangedEventArgs(System.Collections.Specialized.NotifyCollectionChangedAction.Add, mPack, Count));
			Count++;
			return new Actions.CreateAction(newPack);
		}

		private void AppendPack(Packs.INavigable addedPack, ref Packs.INavigable start)
		{
			Packs.INavigable previous = null;
			Packs.INavigable current = start;
			while (current != null)
			{
				previous = current;
				current = current.After;
			}
			if (previous == null)
			{
				start = addedPack;
			}
			else
			{
				previous.After = addedPack;
				addedPack.Before = previous;
			}
		}

		public Actions.IAction MovePack(Packs.IPack pack, Packs.MoveDirection direction, int offset = 1)
		{
			if (offset < 0)
				throw new ArgumentOutOfRangeException("offset", "Offset must be a positive integer.");
			if (offset == 0)
				return new Actions.MoveAction(pack);
			if (direction == Packs.MoveDirection.Up)
			{
				return MoveUp(pack, offset);
			}
			else
			{
				return MoveDown(pack, offset);
			}
		}

		private Actions.IAction MoveUp(Packs.IPack pack, int offset)
		{
			Packs.INavigable moved = pack as Packs.INavigable;
			int oldIndex = getIndex(moved);
			if (moved == destinationEntryPoint)
				return null;
			Packs.INavigable insertPoint = moved;
			while ((insertPoint != null) && (offset > 0))
			{
				offset--;
				insertPoint = insertPoint.Before;
			}
			moved.Before.After = moved.After;
			if (moved.After != null)
				moved.After.Before = moved.Before;
			if ((insertPoint == null) || (insertPoint == destinationEntryPoint))
			{
				moved.Before = null;
				moved.After = destinationEntryPoint;
				destinationEntryPoint.Before = moved;
				destinationEntryPoint = moved;
			}
			else
			{
				moved.Before = insertPoint.Before;
				moved.After = insertPoint;
				moved.Before.After = moved;
				insertPoint.Before = moved;
			}
			OnCollectionChanged(new System.Collections.Specialized.NotifyCollectionChangedEventArgs(System.Collections.Specialized.NotifyCollectionChangedAction.Move, pack, getIndex(moved), oldIndex));
			if (pack is Packs.DeletedPack)
				return null;
			return new Actions.MoveAction(pack);
		}

		private Actions.IAction MoveDown(Packs.IPack pack, int offset)
		{
			Packs.INavigable moved = pack as Packs.INavigable;
			int oldIndex = getIndex(moved);
			if (moved.After == null)
				return null;
			Packs.INavigable previousInsertPoint = null;
			Packs.INavigable insertPoint = moved;
			while ((insertPoint != null) && (offset > 0))
			{
				offset--;
				previousInsertPoint = insertPoint;
				insertPoint = insertPoint.After;
			}
			moved.After.Before = moved.Before;
			if (moved.Before != null)
				moved.Before.After = moved.After;
			else
				destinationEntryPoint = moved.After;
			if (insertPoint == null)
				insertPoint = previousInsertPoint;
			moved.Before = insertPoint;
			moved.After = insertPoint.After;
			if (moved.After != null)
				moved.After.Before = moved;
			insertPoint.After = moved;
			OnCollectionChanged(new System.Collections.Specialized.NotifyCollectionChangedEventArgs(System.Collections.Specialized.NotifyCollectionChangedAction.Move, pack, getIndex(moved), oldIndex));
			if (pack is Packs.DeletedPack)
				return null;
			return new Actions.MoveAction(pack);
		}

		public Actions.IAction DeletePack(Packs.IPack pack)
		{
			if (pack is Packs.DeletedPack)
				return null;
			Packs.INavigable source = pack as Packs.INavigable;
			Packs.DeletedPack deleted = new Packs.DeletedPack() { Before = source.Before, After = source.After, Source = pack };
			source.Destination = deleted;
			source.Before = null;
			source.After = null;
			if (deleted.Before != null)
				deleted.Before.After = deleted;
			else
				destinationEntryPoint = deleted;
			if (deleted.After != null)
				deleted.After.Before = deleted;
			OnCollectionChanged(new System.Collections.Specialized.NotifyCollectionChangedEventArgs(System.Collections.Specialized.NotifyCollectionChangedAction.Replace, deleted, source, getIndex(deleted)));
			return new Actions.DeleteAction(deleted);
		}

		public Actions.IAction EditPack(Packs.IPack pack, Packs.EditedPack data)
		{
			if (pack is Packs.DeletedPack)
				return null;
			Packs.INavigable source = pack as Packs.INavigable;
			Packs.EditedPack edited = source.Source as Packs.EditedPack;
			if (edited == null)
			{
				edited = new Packs.EditedPack();
				edited.Source = source.Source;
				(edited.Source as Packs.INavigable).Destination = edited;
				source.Source = edited;
				edited.Destination = pack;
			}
			edited.Description = data.Description;
			edited.Note = data.Note;
			return new Actions.EditAction(edited);
		}

		private int getIndex(Packs.INavigable element)
		{
			int index = 0;
			while (element.Before != null)
			{
				index++;
				element = element.Before;
			}
			return index;
		}
	}
}
